#  This file is part of OpenLightingDesigner.
# 
#  OpenLightingDesigner is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  OpenLightingDesigner is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with OpenLightingDesigner.  If not, see <http://www.gnu.org/licenses/>.
#
# Device.py
# Copyright (c) 2010 - 2011 Matthew Reid

import sys
import gc
from Bases import setID
from Bases import OSCBaseObject
from ..Attributes import Attribute
from ..Groups.AttributeGroup import DimmerGroup, ColorGroup, PositionGroup, ParameterGroup

propnames = ['name', 'manufacturer', 'description', 'personality_mode']

class Device(OSCBaseObject):
    '''Base for all devices.
    :Parameters:
        'name' : default is empty
        'id' : Universal ID.  Default is None (which assigns a new uuid)
        'description' : default is empty
        'manufacturer' : default is empty
        'channel_count' : int, maximum number of channels for the device.
            default is 0
        'class_name' : class specification (in progress)
        'from_xml' : xml string which, if specified, the xml will be loaded
            into the device.  default is None.
    '''
    
    _saved_class_name = 'Device'
    _saved_attributes = ['base_class', 'id']
    _Properties = dict(zip(propnames, [{'type':str, 'default':''}]*len(propnames)))
    _SettingsProperties = propnames
    #_ChildGroups = {'Attributes':{}}
    def __init__(self, **kwargs):
        self._name = None
        self._manufacturer = None
        self.DeviceSystem = kwargs.get('DeviceSystem')
        self.root_category = self.DeviceSystem.root_category.subcategories['device']
        #kwargs.setdefault('osc_address', str(kwargs.get('Index')))
        super(Device, self).__init__(**kwargs)
        self.register_signal('new_attribute', 'del_attribute', 'child_added')
        if 'deserialize' not in kwargs:
            self.name           = kwargs.get('name', '')
            self.manufacturer   = kwargs.get('manufacturer', '')
            self.description    = kwargs.get('description', '')
            self.personality_mode = kwargs.get('personality_mode', '')
            self.id = setID(kwargs.get('id'))
            if hasattr(self, 'base_class') is False:
                self.base_class = kwargs.get('base_class')
            categories = kwargs.get('categories',{})
            for category in categories.itervalues():
                self.add_category(category)
            
    def add_osc_Props(self):
        for prop in self.Properties.itervalues():
            self.add_osc_handler(Property=prop)
            
    def ChildGroup_prepare_child_instance(self, childgroup, cls, **kwargs):
        kwargs.setdefault('root_category', self.root_category.parent.subcategories['attribute'])
        kwargs.setdefault('parent_device', self)
        return cls, kwargs
        
    def __repr__(self):
        vals = [self.__class__.__name__]
        attrs = ['name', 'id', 'Index', 'ParentEmissionThread']
        vals.extend([getattr(self, attr, None) for attr in attrs])
        s = '<%s %s (%s) index: %s, ParentEmissionThread: %r>' % tuple(vals)
        return s

class DefinedDevice(Device):
    '''A Device Definition
    
    :Events:
        'new_attribute' (device_id, attribute_id)
    '''
    _saved_class_name = 'DefinedDevice'
    _Properties = {'channel_count':dict(default=0, fvalidate='_ch_count_validate'), 
                   'pan_range':dict(type=int), 
                   'tilt_range':dict(type=int)}
    _SettingsProperties = ['channel_count', 'pan_range', 'tilt_range']
    _ChildGroups = {'Attributes':dict(child_class=Attribute.DefinitionAttribute, 
                                      send_child_updates_to_osc=True)}
    _saved_child_objects = ['Attributes']
    def __init__(self, **kwargs):
        self.GlobalAttributes = kwargs['DeviceSystem'].GlobalAttributes
        d = kwargs.get('deserialize', {}).get('attrs', kwargs)
        osc_address = d.get('osc_address')
        if not osc_address:
            manuf = d.get('manufacturer', '')
            name = d.get('name', '')
            if len(name):
                if len(manuf):
                    osc_address = '-'.join([manuf, name])
                else:
                    osc_address = name
            else:
                osc_address = str(d.get('Index'))
            #print self, osc_address
            kwargs['osc_address'] = osc_address
        super(DefinedDevice, self).__init__(**kwargs)
        #print self.osc_address
        self.register_signal('chan_update', 'group_added', 'definition_update')
        if 'deserialize' not in kwargs:
            self.channel_count  = kwargs.get('channel_count', 0)
        self.add_osc_Props()
        self._update_attributes()
        #keys = ['child_added', 'child_removed', 'child_index_changed']
        #self.Attributes.bind(**dict(zip(keys, [self.on_attribute_index_changed]*len(keys))))
        self.Attributes.bind(child_update=self.on_Attributes_child_update)
        
    def add_attribute(self, **kwargs):
        '''
        :Parameters:
            'attribute' : instance of a global attribute to copy from.
                this will soon be changed
            'chan_index' : int, relative channel index for the new attribute
            'id' : id for the new attribute.  Default is None (which assigns a new uuid)
        '''
        attribute = kwargs.get('attribute')
        #kwargs.update({'parent_device_id':self.id, 'osc_address':attribute.name, 
        #               'parent_device':self})
        kwargs['GlobalAttribute'] = attribute
        #kwargs.update({'root_category':self.root_category.parent.subcategories['attribute']})
        new_attribute = self.Attributes.add_child(Attribute.DefinitionAttribute, **kwargs)
        self.emit('new_attribute', device_id=self.id, attribute_id=new_attribute.id)
        self.emit('child_added', device_id=self.id, attribute_id=new_attribute.id)
        return new_attribute
    
    def del_attribute(self, **kwargs):
        attrib_id = kwargs.get('id')
        attribute = self.Attributes.get(attrib_id)
        if attribute:
            attribute.unbind(self.on_attribute_index_changed)
            self.Attributes.del_child(attribute)
            self._update_attributes()
            self.emit('del_attribute', device_id=self.id, attribute_id=attrib_id, device=self)
        
    def _ch_count_validate(self, value):
        return value >= self.Attributes.find_max_index()
    
    def _update_attributes(self):
        max_chan = self.Attributes.find_max_index()
        if max_chan and self.Attributes.indexed_items[max_chan].sixteen_bit:
            max_chan += 1
        if max_chan > self.channel_count:
            self.channel_count = max_chan
        
    def on_Attributes_child_update(self, **kwargs):
        self._update_attributes()
        self.emit('chan_update', device=self, attribute=kwargs.get('obj'))
        
    def ChildGroup_prepare_child_instance(self, childgroup, cls, **kwargs):
        d = kwargs.get('deserialize')
        if d is not None and d['saved_class_name'] == 'DefinitionAttribute':
            gl_attrib = self.GlobalAttributes.get(d['attrs']['GlobalAttribute_id'])
            kwargs['GlobalAttribute'] = gl_attrib
        #if cls is None:
        #    cls = Attribute.DefinitionAttribute
        return super(DefinedDevice, self).ChildGroup_prepare_child_instance(childgroup, cls, **kwargs)
        
#    def deserialize_Attribute(self, d):
#        if d['saved_class_name'] == 'DefinitionAttribute':
#            kwargs = {}
#            if self.osc_enabled:
#                kwargs.update({'osc_address':d['attrs'].get('osc_address')})
#            kwargs.update({'root_category':self.root_category.parent.subcategories['attribute'], 
#                           'parent_device':self})
#            gl_attrib = self.GlobalAttributes.get(d['attrs']['GlobalAttribute_id'])
#            kwargs.update({'GlobalAttribute':gl_attrib, 'deserialize':d})
#            attribute = self.Attributes.add_child(Attribute.DefinitionAttribute, **kwargs)
#            return attribute
#        #return super(Device, self)._deserialize_child(d)
        

class PatchedDevice(Device):
    '''A Device that has been patched
    
    :Parameters:
        '_DefinedDevice_' : instance of a device definition to use for the new device.
            this will change soon
        'definition_id' : id of the device definition used to make this device. (for later use)
        'device_num' : int, index of the device to be used in the UI (for organization)
        'universe_id' : id of the DMX universe this device belongs to
        'start_chan' : int, starting DMX channel
        'from_xml' : xml string which, if specified, the xml will be loaded
            into the device.  default is None.
    '''
    _saved_class_name = 'PatchedDevice'
    _saved_attributes = ['device_num', 'universe_id', 'start_chan', 'definition_id', 'pan_invert', 'tilt_invert', 'power_control_attribute_id']
    _saved_child_objects = ['Groups']
    signals_to_register = ['device_num_update', 'check_device_num']
    _Properties = {'universe_id':dict(type=str), 
                   'start_chan':dict(type=int), 
                   'power_control_attribute':{}, 
                   'Power':dict(default=False), 
                   'ActivePalettes':dict(default={}, quiet=True), 
                   'pan_invert':dict(default=False), 
                   'tilt_invert':dict(default=False)}
    group_classes = [PositionGroup, ColorGroup, DimmerGroup]
    _ChildGroups = {'Attributes':dict(child_class=Attribute.PatchedAttribute)}
    def __init__(self, **kwargs):
        self.ActivePaletteData = {}
        self.attr_by_chan = {}
        self.ungrouped_attributes = {}
        prebind = kwargs.get('prebind', {})
        prebind.update({'pan_invert':self._on_pan_invert_set, 
                        'tilt_invert':self._on_tilt_invert_set})
        kwargs['prebind'] = prebind
        kwargs.setdefault('osc_address', kwargs.get('Index'))
        super(PatchedDevice, self).__init__(**kwargs)
        self.bind(device_num=self._on_device_num_set, 
                  power_control_attribute=self.on_power_control_attribute_set, 
                  Power=self.on_Power_set)
        self.add_osc_child_nodes()
        if 'deserialize' not in kwargs:
            self.definition = kwargs.get('definition')
            self.definition_id = getattr(self.definition, 'id', None)
            self.start_chan = kwargs.get('start_chan')
            self.universe_id = kwargs.get('universe_id')
            self.power_control_attribute_id = kwargs.get('power_control_attribute_id')
            self.power_control_attribute = kwargs.get('power_control_attribute')
            self.Groups = {}
            gr_kwargs = dict(osc_parent_node=self.group_osc_node, 
                             root_category=self.root_category.parent.subcategories['attribute'], 
                             ParentEmissionThread=self.ParentEmissionThread)
            for grpcls in self.group_classes:
                key = grpcls._saved_class_name
                group = grpcls(**gr_kwargs)
                self.Groups.update({key:group})
            for key in ['pan_range', 'tilt_range']:
                value = getattr(self.definition, key)
                if value is not None:
                    setattr(self.Groups['Position'], key, value)
        else:
            self.definition = self.DeviceSystem.DefinedDevices.get(self.definition_id)
        if self.definition:
            self._init_attributes(self.definition.Attributes.copy())
            for key in ['name', 'description']:
                if getattr(self, key) == '':
                    setattr(self, key, getattr(self.definition, key))
            self.definition.connect('definition_update', self.on_definition_update)
        self.add_osc_Props()
            
    @property
    def device_num(self):
        return self.Index
    @device_num.setter
    def device_num(self, value):
        self.Index = value

    def _on_device_num_set(self, **kwargs):
        value = kwargs.get('value')
        old = kwargs.get('old')
        self.emit('device_num_update', device=self, value=value, old=old)
        
    def _device_num_validate(self, value):
        return not(value in self.DeviceSystem.DevicesByNum)
            
    @property
    def end_chan(self):
        start = self.start_chan
        if start is not None:
            return start + self.channel_count - 1
        return None
        
    def _on_pan_invert_set(self, **kwargs):
        value = kwargs.get('value')
        if hasattr(self, 'Groups') and 'Position' in self.Groups:
            self.Groups['Position'].pan_invert = value
            
    def _on_tilt_invert_set(self, **kwargs):
        value = kwargs.get('value')
        if hasattr(self, 'Groups') and 'Position' in self.Groups:
            self.Groups['Position'].tilt_invert = value
    
    def repatch(self, **kwargs):
        univ_id = kwargs.get('universe_id')
        start = kwargs.get('start_chan')
        self.universe_id = univ_id
        self.start_chan = start
        self.attr_by_chan.clear()
        for attrib in self.Attributes.itervalues():
            attrib.repatch(**kwargs)
            self.attr_by_chan.update({attrib.chan:attrib})
        
    def unpatch(self):
        for group in self.Groups.itervalues():
            group.unlink()
        for group in self.ParameterGroups.itervalues():
            group.unlink()
        for attrib in self.Attributes.itervalues():
            attrib.unpatch()
        
    def add_active_palette(self, **kwargs):
        palette = kwargs.get('palette')
        category_palette = kwargs.get('category_palette')
        groups = kwargs.get('groups')
        is_parent = kwargs.get('is_parent', False)
        category = category_palette.category.name
        self.del_active_palette(category=category, set_to_none=False)
        self.ActivePaletteData[category] = {'palette':palette, 
                                            'category_palette':category_palette, 
                                            'groups':groups, 
                                            'is_parent':is_parent}
        self.ActivePalettes[category] = category_palette.Index
        
    def del_active_palette(self, **kwargs):
        category = kwargs.get('category')
        set_to_none = kwargs.get('set_to_none', True)
        if not category:
            palette = kwargs.get('palette')
            category = palette.category.name
        d = self.ActivePaletteData.get(category)
        if d is not None and category in self.ActivePaletteData:
            d['category_palette'].EffectsSystem.clear_palette(id=d['palette'].id)
            if d['groups']:
                for key in d['groups'].keys()[:]:
                    group = d['groups'][key]
                    group.unlink()
                    del d['groups'][key]
            del self.ActivePaletteData[category]
            if set_to_none:
                self.ActivePalettes[category] = None
            
    def find_ActivePalette_parent(self, **kwargs):
        category = kwargs.get('category')
        if self.ActivePalettes.get(category) is None:
            return False
        if self.ActivePaletteData[category]['is_parent']:
            return self
        return False
            
    def on_power_control_attribute_set(self, **kwargs):
        attribute = kwargs.get('value')
        if attribute:
            self.power_control_attribute_id = attribute.id
            group = PowerControlGroup(device=self, attribute_name='Power', 
                                      osc_parent_node=self.group_osc_node)
            group.add_member(**{attribute.id:attribute})
            self.Groups.update({group.name:group})
        
    def on_Power_set(self, state):
        group = self.Groups.get('Power')
        if group:
            group.value = state            
        
    def add_osc_child_nodes(self):
        if not hasattr(self, 'group_osc_node'):
            self.group_osc_node = self.osc_node.add_child(name='Groups')
        
    def request_values_from_osc(self, **kwargs):
        for attrib in self.Attributes.itervalues():
            attrib.request_values_from_osc(**kwargs)
            
    def _deserialize_child(self, d, **kwargs):
        if d['attrs']['base_class'] == 'Group':
            self.add_osc_child_nodes()
            cls_d = dict(zip([cls._saved_class_name for cls in self.group_classes], self.group_classes))
            obj = cls_d[d['saved_class_name']](deserialize=d, osc_parent_node=self.group_osc_node, 
                                               root_category=self.root_category.parent.subcategories['attribute'])
                                               ## TODO: renable this when you figure out why it locks up
                                               #ParentEmissionThread=self.ParentEmissionThread)
            if obj.saved_class_name == 'Position':
                obj.pan_invert = self.pan_invert
                obj.tilt_invert = self.tilt_invert
            return obj
        return super(PatchedDevice, self)._deserialize_child(d, **kwargs)
    
    @property
    def channel_count(self):
        return self.definition.channel_count

    def ChildGroup_prepare_child_instance(self, childgroup, cls, **kwargs):
        if 'deserialize' in kwargs:
            return None, kwargs
        return super(PatchedDevice, self).ChildGroup_prepare_child_instance(childgroup, cls, **kwargs)
    def _init_attributes(self, attributes):
        new_kwargs = {'universe_id':self.universe_id, 'parent_device_id':self.id}
        self.Attributes.clear()
        self.attr_by_chan.clear()
        for key, val in attributes.iteritems():
            chan = self.start_chan + val.chan_index - 1
            new_kwargs.update({'chan':chan, 'osc_address':val.name, 'DefinitionAttribute':val, 
                               'Index':val.Index})
            new_attribute = self.Attributes.add_child(Attribute.PatchedAttribute, **new_kwargs)
            self.attr_by_chan.update({new_attribute.chan:new_attribute})
            if new_attribute.sixteen_bit:
                lsb = new_attribute.lsb_attribute
                self.attr_by_chan.update({lsb.chan:lsb})
            self.emit('new_attribute', device_id=self.id, attribute_id=new_attribute.id)
            self.emit('child_added', device_id=self.id, attribute_id=new_attribute.id)
        for g in self.Groups.itervalues():
            g.device = self
            g.search_for_members()
        if not hasattr(self, 'ParameterGroups'):
            self.ParameterGroups = {}
        for key, val in self.Attributes.iteritems():
            if len(val.users) == 0:
                self.ungrouped_attributes.update({key:val})
            group = ParameterGroup(device=self, attribute_name=val.name, 
                                   osc_parent_node=self.group_osc_node)
                                   ## TODO: renable this when you figure out why it locks up
                                   #ParentEmissionThread=self.ParentEmissionThread)
            if group.valid:
                self.ParameterGroups.update({val.name:group})
                
    def on_definition_update(self, **kwargs):
        pass
